﻿using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Goldenlight.Collections;
using Microsoft.Goldenlight.Resources;

namespace Microsoft.Goldenlight.Framework
{
    /// <summary>
    /// 	Manager of a collection of RegisterableObjects. Every object must be registered with the object manager before being used and unregistered after use.
    /// </summary>
    /// <remarks>
    /// 	Objects can be looked up by name or id. Anything derived from RegisterableObject can also be looked up by name. When objects are unregistered from manager, any GameSafePtr pointing to them is nulled out.
    /// </remarks>
    public class ObjectManager
    {
        #region Fields & Properties

        private readonly IServiceProvider _serviceProvider;

        /// <summary>
        /// 	Gets the service provider associated with the ObjectManager.
        /// </summary>
        public IServiceProvider ServiceProvider
        {
            get
            {
                return _serviceProvider;
            }
        }

        private Container _currentContainer;

        /// <summary>
        /// 	The currently selected container on the manager.
        /// </summary>
        public Container CurrentContainer
        {
            get
            {
                return _currentContainer;
            }
            set
            {
                if (value == null)
                {
                    throw new ArgumentNullException("value");
                }
                if (!value.IsRegistered || value.Manager != this)
                {
                    throw new ArgumentException(Resx.CantSetCurrentContainer, "value");
                }
                _currentContainer = value;
            }
        }

        #endregion

        #region Constructors

        /// <summary>
        /// 	Initializes a new instance of the object manager class.
        /// </summary>
        /// <param name="serviceProvider"> The service provider that the ObjectManager should use to locate services. </param>
        public ObjectManager(IServiceProvider serviceProvider)
        {
            if (serviceProvider == null)
            {
                throw new ArgumentNullException("serviceProvider");
            }
            _serviceProvider = serviceProvider;
        }

        #endregion

        #region Object Register & Unregister

        #region Fields & Properties

        private readonly Dictionary<uint, RegisterableObject> _registeredObjects = new Dictionary<uint, RegisterableObject>(0x1000);
        private uint _nextId = 1;

        #endregion

        #region  Register & Unregister

        /// <summary>
        /// 	Registers a RegisterableObject with the manager.
        /// </summary>
        /// <remarks>
        /// 	All RegisterableObjects need to be registered before being used. Objects cannot be registered more than once. If the object is marked as a template object registration will fail.
        /// </remarks>
        /// <param name="registerableObject"> The object to be registered. </param>
        /// <returns> If the object or any of it's behaviors fails to properly registered, this method will return false and the entire object will not be registered. </returns>
        public bool Register(RegisterableObject registerableObject)
        {
            if (registerableObject != null && registerableObject.Id == 0 && !registerableObject.IsRegistered && !registerableObject.IsTemplate)
            {
                registerableObject.Id = _nextId++;
                registerableObject.Manager = this;
                _registeredObjects[registerableObject.Id] = registerableObject;
                if (!registerableObject.Register())
                {
                    Unregister(registerableObject);
                    return false;
                }
                return true;
            }
            return false;
        }

        /// <summary>
        /// 	Unregisters a RegisterableObject.
        /// </summary>
        /// <remarks>
        /// 	The object must be registered or else this is an error.
        /// </remarks>
        /// <param name="registerableObject"> The object to unregister. </param>
        /// <returns> True if unregister is successfully. </returns>
        public void Unregister(RegisterableObject registerableObject)
        {
            Unregister(registerableObject, true);
        }

        /// <summary>
        /// 	Unregisters a RegisterableObject.
        /// </summary>
        /// <remarks>
        /// 	The object must be registered or else this is an error.
        /// </remarks>
        /// <param name="registerableObject"> The object to unregister. </param>
        /// <param name="dispose"> Unregistering an object will perform any cleanup that is needed and will clear out GameSafePtr's that point to this object. </param>
        /// <returns> True if unregister is successfully. </returns>
        public void Unregister(RegisterableObject registerableObject, bool dispose)
        {
            if (registerableObject != null && registerableObject.Id != 0 && _registeredObjects.ContainsKey(registerableObject.Id) && registerableObject.IsRegistered && registerableObject.Manager == this && !registerableObject.IsTemplate)
            {
                var container = registerableObject as Container;
                if (container != null && container == _currentContainer)
                {
                    _currentContainer = null;
                }
                if (_registeredObjects.Remove(registerableObject.Id))
                {
                    registerableObject.Manager = null;
                    registerableObject.Id = 0;
                    registerableObject.Unregister();
                    if (dispose)
                    {
                        registerableObject.Dispose();
                    }
                }
            }
        }

        #endregion

        #endregion

        #region Object Deletion

        #region  Fields & Properties

        private readonly DoublyLinkedList<RegisterableObject> _markedDeleteObjects = new DoublyLinkedList<RegisterableObject>();

        #endregion

        internal void AddMarkedObject(RegisterableObject registerableObject)
        {
            if (registerableObject != null && !_markedDeleteObjects.Contains(registerableObject))
            {
                _markedDeleteObjects.AddLast(registerableObject);
            }
        }

        internal void RemoveMarkedObject(RegisterableObject registerableObject)
        {
            if (registerableObject != null)
            {
                _markedDeleteObjects.Remove(registerableObject);
            }
        }

        /// <summary>
        /// 	Unregister all the objects which have been marked for deletion.
        /// </summary>
        /// <remarks>
        /// 	This method is periodically called by the engine and does not need to be called by the user.
        /// </remarks>
        public void DeleteMarkedObjects()
        {
            foreach (var markedObject in _markedDeleteObjects)
            {
                Unregister(markedObject);
            }
            _markedDeleteObjects.Clear();
        }

        #endregion

        #region Object Find

        #region Find Single Object

        /// <summary>
        /// 	Searches for a RegisterableObject in the manager.
        /// </summary>
        /// <param name="id"> Id of the object to look up. </param>
        /// <returns> A RegisterableObject in the manager. </returns>
        public RegisterableObject FindObject(uint id)
        {
            RegisterableObject foundObject;
            return _registeredObjects.TryGetValue(id, out foundObject) ? foundObject : null;
        }

        /// <summary>
        /// 	Searches for a RegisterableObject in the manager.
        /// </summary>
        /// <remarks>
        /// 	This method can find item which are not RegisterableObject. To find RegisterableObject only use the templated version.
        /// </remarks>
        /// <param name="name"> Name of the object to look up. </param>
        /// <returns> A RegisterableObject in the manager. </returns>
        public RegisterableObject FindObject(string name)
        {
            if (string.IsNullOrEmpty(name))
            {
                throw new ArgumentNullException("name");
            }
            return _registeredObjects.Values.FirstOrDefault(registerableObject => registerableObject.Name == name);
        }

        /// <summary>
        /// 	Searches for an object of type T.
        /// </summary>
        /// <typeparam name="T"> The type of the RegisterableObject. </typeparam>
        /// <returns> A object of the specified type in the manager. </returns>
        public T FindObject<T>() where T : RegisterableObject
        {
            return _registeredObjects.Values.OfType<T>().FirstOrDefault();
        }

        /// <summary>
        /// 	Searches for an object of type T.
        /// </summary>
        /// <typeparam name="T"> The type of the RegisterableObject. </typeparam>
        /// <param name="id"> Id of the object to look up. </param>
        /// <returns> A object of the specified type in the manager. </returns>
        public T FindObject<T>(uint id) where T : RegisterableObject
        {
            RegisterableObject foundObject;
            return _registeredObjects.TryGetValue(id, out foundObject) ? foundObject as T : null;
        }

        /// <summary>
        /// 	Searches for an object of type T.
        /// </summary>
        /// <typeparam name="T"> The type of the RegisterableObject. </typeparam>
        /// <param name="name"> Name of the object to look up. </param>
        /// <returns> A object of the specified type in the manager. </returns>
        public T FindObject<T>(string name) where T : RegisterableObject
        {
            if (string.IsNullOrEmpty(name))
            {
                throw new ArgumentNullException("name");
            }
            return _registeredObjects.Values.OfType<T>().FirstOrDefault(registerableObject => registerableObject.Name == name);
        }

        #endregion

        #region Find Objects

        /// <summary>
        /// 	Searches for and populates the results list with objects of the specified type.
        /// </summary>
        /// <typeparam name="T"> The type of the RegisterableObject. </typeparam>
        /// <returns> A list of objects of the specified type. </returns>
        public IEnumerable<T> FindObjects<T>() where T : RegisterableObject
        {
            return _registeredObjects.Values.OfType<T>();
        }

        /// <summary>
        /// 	Searches for and populates the results list with objects of the specified type.
        /// </summary>
        /// <typeparam name="T"> The type of the RegisterableObject. </typeparam>
        /// <param name="predicate"> Restricts the range of returned objects by specified clause. </param>
        /// <returns> A list of objects of the specified type. </returns>
        public IEnumerable<T> FindObjects<T>(Func<T, bool> predicate) where T : RegisterableObject
        {
            return _registeredObjects.Values.OfType<T>().Where(predicate);
        }

        /// <summary>
        /// 	Searches for and populates the results list with objects of the specified type.
        /// </summary>
        /// <typeparam name="T"> The type of the RegisterableObject. </typeparam>
        /// <param name="results"> A list of objects of the specified type. </param>
        public void FindObjects<T>(ref List<T> results) where T : RegisterableObject
        {
            if (results == null)
            {
                throw new ArgumentNullException("results");
            }
            results.AddRange(FindObjects<T>());
        }

        /// <summary>
        /// 	Searches for and populates the results list with objects of the specified type.
        /// </summary>
        /// <typeparam name="T"> The type of the RegisterableObject. </typeparam>
        /// <param name="predicate"> Restricts the range of returned objects by specified clause. </param>
        /// <param name="results"> A list of objects of the specified type. </param>
        public void FindObjects<T>(ref List<T> results, Func<T, bool> predicate) where T : RegisterableObject
        {
            if (results == null)
            {
                throw new ArgumentNullException("results");
            }
            results.AddRange(FindObjects(predicate));
        }

        #endregion

        #region Find Objects By Group

        /// <summary>
        /// 	Searches for and populates the results list with objects of the specified type by specified Group.
        /// </summary>
        /// <typeparam name="T"> The type of the GameObject. </typeparam>
        /// <param name="group"> The specified group. </param>
        /// <returns> A list of objects of the specified type. </returns>
        public IEnumerable<T> FindObjectsByGroup<T>(Group group) where T : GameObject
        {
            return _registeredObjects.Values.OfType<T>().Where(gameObject => gameObject.OfGroup(group));
        }

        /// <summary>
        /// 	Searches for and populates the results list with objects of the specified type by specified Group.
        /// </summary>
        /// <typeparam name="T"> The type of the GameObject. </typeparam>
        /// <param name="group"> The specified group. </param>
        /// <param name="predicate"> Restricts the range of returned objects by specified clause. </param>
        /// <returns> A list of objects of the specified type. </returns>
        public IEnumerable<T> FindObjectsByGroup<T>(Group group, Func<T, bool> predicate) where T : GameObject
        {
            return _registeredObjects.Values.OfType<T>().Where(gameObject => gameObject.OfGroup(group)).Where(predicate);
        }

        /// <summary>
        /// 	Searches for and populates the results list with objects of the specified type by specified Group.
        /// </summary>
        /// <typeparam name="T"> The type of the GameObject. </typeparam>
        /// <param name="group"> The specified group. </param>
        /// <param name="results"> A list of objects of the specified type. </param>
        public void FindObjectsByGroup<T>(ref List<T> results, Group group) where T : GameObject
        {
            if (results == null)
            {
                throw new ArgumentNullException("results");
            }
            results.AddRange(FindObjectsByGroup<T>(group));
        }

        /// <summary>
        /// 	Searches for and populates the results list with objects of the specified type by specified Group.
        /// </summary>
        /// <typeparam name="T"> The type of the GameObject. </typeparam>
        /// <param name="group"> The specified group. </param>
        /// <param name="predicate"> Restricts the range of returned objects by specified clause. </param>
        /// <param name="results"> A list of objects of the specified type. </param>
        public void FindObjectsByGroup<T>(ref List<T> results, Group group, Func<T, bool> predicate) where T : GameObject
        {
            if (results == null)
            {
                throw new ArgumentNullException("results");
            }
            results.AddRange(FindObjectsByGroup(group, predicate));
        }

        #endregion

        #endregion

        #region Group Operations

        #region Fields & Properties

        private readonly Dictionary<string, Group> _namesToGroups = new Dictionary<string, Group>(64);
        private readonly Dictionary<Group, string> _groupsToNames = new Dictionary<Group, string>(64);

        /// <summary>
        /// 	Get all primitive group names;
        /// </summary>
        public List<string> GroupNames
        {
            get
            {
                return _groupsToNames.Values.ToList();
            }
        }

        #endregion

        /// <summary>
        /// 	Finds an existing Group by name or registers a new version if one doesn't already exist.
        /// </summary>
        /// <remarks>
        /// 	If the number of registered types exceeds the max (64) or if the group manager is locked an empty group will be returned.
        /// </remarks>
        /// <param name="name"> Name of group to search for. </param>
        /// <returns> A primitive group. </returns>
        public Group GetGroup(string name)
        {
            Group primitiveGroup;
            if (!_namesToGroups.TryGetValue(name, out primitiveGroup))
            {
                if (_namesToGroups.Count >= 64)
                {
                    return Group.None;
                }
                primitiveGroup = (ulong) (1 << _namesToGroups.Count);
                _namesToGroups[name] = primitiveGroup;
                _groupsToNames[primitiveGroup] = name;
            }
            return primitiveGroup;
        }

        /// <summary>
        /// 	Finds the description (names) by an existing Group that can be a primitive or a combined group.
        /// </summary>
        /// <param name="group"> A primitive or combined group. </param>
        /// <returns> Names of group. </returns>
        public string GetGroupDescription(Group group)
        {
            if (!group.IsMultiple)
            {
                string name;
                if (_groupsToNames.TryGetValue(group, out name))
                {
                    return name;
                }
                return string.Empty;
            }
            return _groupsToNames.Where(groupToName => (groupToName.Key & group) != Group.None).Aggregate(string.Empty, (description, groupToName) => string.Format("{0},{1}", description, groupToName.Value)).TrimStart(',');
        }

        #endregion
    }
}